我對牠笑了一笑。牠看起來不像是在裝腔作勢。牠的態度顯示牠清楚知道自己在記憶力方面是有多麼的與眾不同。不過嘛,這也說不準,更別說牠可是隻草泥馬。只是在程度上的差別而已,草泥馬全都是些瘋狂的生物。
~節錄自《賴田捕手》第八章
今天的程式碼我們需要用到一個叫 numpy 的套件。這個套件是 Python 中關於數值計算相當重要的套件,因此 Anaconda 本身已經有內建這個套件了。不過老樣子,在我們先前新建置的 ironman_env 空空如也的環境裡(見第01天),是找不到這個套件的,所以在開啟 Jupyter Notebook 開始練習今天的程式碼之前,我們要先來安裝這個套件。
這個也不難,我們複習一下怎麼安裝套件在特定的環境裡:
(bash) C:\Users\MyName>conda activate ironman_env
(ironman_env) C:\Users\MyName>conda install numpy
首先,利用conda activate ironman_env
進入到我們建置的環境,接著輸入conda install numpy
,numpy
是我們這次要安裝的套件名稱。系統會幫我們分析當前的環境以及是否需要一併安裝哪些相關套件。分析完畢之後,熟悉的:
Proceed ([y]/n)?
按下 Enter ,安裝流程結束之後,會看到以下資訊,代表成功安裝了我們需要的套件。Yeah!
Preparing transaction: done
Verifying transaction: done
Executing transaction: done
(ironman_env) C:\Users\MyName>
可以開始練習今天的程式碼囉。第一件事是引入(import)我們需要用的套件:
In [1]: import numpy as np
這行程式碼什麼意思呢?前面應該不難理解,就是先引入numpy
這個套件,方便我們等等取用。但我實在是太懶惰了,每次要用都要輸入numpy
來呼叫它?於是我幫它取了個暱稱np
,從此以後,只要我輸入np
,Python 就知道我是在呼叫numpy
了!
還記得我小時候有一部電影相當有名,中文片名叫做《雨人》(Rain Man)➀,由湯姆克魯斯(Tom Cruise)跟達斯汀霍夫曼(Dustin Hoffman)主演。內容描述一個有自閉症卻對數字天賦異稟的人,在賭城裡闖蕩的故事。我原本以為電影就是電影,編編故事而已,真實世界哪有這麼神的傢伙?結果我卻在家裡飼養的草泥馬當中發現了這樣一個逸才。天啊這不是上天指引的一條發財的明路嗎?事不遲疑,我馬上帶著牠來到拉斯維加斯(?)打算大撈一筆。
骰子遊戲規則簡單明瞭,草泥馬也能夠理解,因此我們決定從賭骰子開始。首先我們需要一顆骰子。
In [2]: def 骰子():
點數 = np.ceil(np.random.rand()*6).astype(int)
return 點數
太兇狠了一上來就是一個函數。所謂的函數,數學上的解釋是:將一個輸入值透過操作,映射到輸出值的過程,就叫做函數➁。可以白話一點嗎?再一次:所謂函數,將參數吃進肚子,經過消化,回傳結果給你的,就是函數。所以函數有三個特徵:
這要怎麼用程式碼來表示呢?請聽我娓娓道來:
def 骰子():
def
這個關鍵字,告訴 Python 接下來我們要做的是一個函數物件。函數物件的標籤,就叫做骰子
。而這個函數需要的參數,就放進()
裡保存。擲骰子這個函數,我們目前還不需要任何參數,所以空著就行。 點數 = np.ceil(np.random.rand()*6).astype(int)
點數
這個標籤貼著。 return 點數
return
這個關鍵字,將代表point
的物件回傳(return)給使用者。 這樣就做出一個骰子了。等等等等,第二行那堆亂七八糟的文字到底在做什麼啊?
擲骰子,在不作弊的前提下,是一個隨機的過程。因此我們需要一個可以隨機傳回數字的工具,來幫助我們模仿骰子。這邊,我用的就是numpy.random
。numpy.random
裡面提供了幾種不同的函數,用來返回隨機的數值。常用的幾種函數如表一。
表一、numpy.random
裡提供的幾種函數
程式碼 | 作用 |
---|---|
rand |
從範圍中隨機抽取數值 |
randint |
從範圍中隨機抽取整數值 |
randn |
以常態分佈的方式,隨機抽取數值(平均為 0,標準差為 1) |
這邊順便說明一個用 Jupyter Notebook 的小技巧。打上想要使用的函數之後,是不是常常像我一樣,忘記該輸入哪些參數,或是每個參數代表的涵意呢?不要害怕,頂多上網查查就好嘛。不過在那之前,可以先試著在函數後面要接著輸入參數的地方,按下Shift + Tab,這時候函數如果有提供使用說明書(Docstring)的話,就會跳出來讓你看個仔細囉,如圖一。
圖一、利用 Shift + Tab 呼叫函數的使用說明書。可以試試壓著 Shift 的同時,再多按幾次 Tab。
好啦,回到我們的np.random.rand()
上面。該函數會回傳一個從 0 開始(包含 0),到 1 為止(不包含 1),中間隨機的一個浮點數。因為我們要用在骰子上,所以我希望把這個隨機的浮點數轉換為1到 6 (因此我用了*6
)。之後用np.ceil()
修飾一下。np.ceil()
會把一個浮點數轉換為大於等於該浮點數的最小整數。舉例來說,2.3 透過np.ceil()
會得到 3.0,而 4.0 透過np.ceil
出來還是 4.0。最後,因為我只想要整數,只想要 4 而不是 4.0,因此用astype(int)
來修飾。這是numpy
物件特有的方法。我說完了,大家再回頭看看程式碼,希望上面的說明能解釋大家的疑惑。另外,聰明的大家,是否看出上面那個骰子
有個神奇的毛病呢?
當然還有很多種其他寫法,大家可以試著用np.random.randint()
並參考其使用說明書,寫一顆比我還好的骰子。
其實我們的骰子
程式碼相當簡單,所以我再把它整理一下如下:
In [3]: def 骰子():
return np.ceil(np.random.rand()*6).astype(int)
盡情丟骰子囉!
In [4]: 骰子()
Out[4]: 4
到了賭場,我們丟了 10 次骰子。有沒有辦法把這 10 次丟到的點數都記錄下來呢?我有記性驚人、對數字展現出過人天賦的草泥馬,而你有 Python,那麼當然沒問題囉!
In [5]: dice_list = []
for i in range(10):
dice_list.append(骰子())
dice_list
Out[5]: [6, 6, 6, 1, 4, 4, 6, 6, 5, 3]
上面那是第一種做法。可是其實 Python 還有更猛的做法:
In [6]: [骰子() for i in range(10)]
Out[6]: [4, 3, 2, 1, 1, 3, 5, 3, 4, 5]
這叫做清單自表(list comprehension)。好啦,翻譯是我亂翻的,大家還是記英文吧。當需要用for
迴圈來製造清單的時候,你就可以試著用 list comprehension。它的用法是這樣子的:
[這裡 for 標籤 in 可數(ˇ)數(ˋ)物件]
後面的for 標籤 in 可數(ˇ)數(ˋ)物件
就是for
迴圈的起始宣告,跟一般的for
迴圈寫法是相同的。比較需要注意的是這裡
。這裡
是每跑一次迴圈要放入清單的項目,可以跟標籤有關係,也可以跟標籤無關。再多看點例子?
In [7]: 這裡 = 100
可數數物件 = range(10)
[這裡 for 標籤 in 可數數物件]
Out[7]: [100, 100, 100, 100, 100, 100, 100, 100, 100, 100]
再多一點。
In [8]: 可數數物件 = range(10)
[標籤 ** 2 for 標籤 in 可數數物件]
Out[8]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
好啦,回到賭場繼續擲我們的骰子囉。
賭場?你少騙我了。哪個賭場在玩擲一顆骰子的遊戲?別太早下結論,才剛開始嘛。我們這就要來擲一堆骰子囉!
In [9]: def 一堆骰子(骰子數量):
return [骰子() for i in range(骰子數量)]
這邊我們設計了一個需要輸入一個參數(骰子數量
)的函數,而輸入的參數則會影響回傳值。
In [10]: 一堆骰子(6)
Out[10]: [3, 1, 1, 6, 2, 6]
我們丟出了 6 顆骰子啦!
到目前為止,我們都在寫一些簡單的函數,只要用一行程式碼就可以寫完。Python 裡面提供了 Lambda函數(Lambda function),專寫這種一行便可結束的程式碼。我們也來試試看。
In [11]: LAMBDA的一堆骰子 = lambda 骰子數量: [骰子() for i in range(骰子數量)]
一堆骰子 = lambda 骰子數量: [骰子() for i in range(骰子數量)]
lambda
宣告我們要寫 Lambda 函數囉。接著後面是一個**「參數: 回傳值」**的組合。這樣就創造出一個函數了。最後我們再把這個函數用LAMBDA的一堆骰子
的標籤貼好。真的只用一行便可結束呢!In [12]: LAMBDA的一堆骰子(6)
Out[12]: [4, 3, 3, 6, 2, 3]
說實在我這樣子寫,可能沒什麼人會覺得 Lambda 函數有什麼特別方便的地方?事實上,Lambda 函數又被稱為匿名函數,它們更常被用在數據分析的程式碼裡面。我會在這邊介紹,是希望大家在其他地方看到 Lambda 函數的時候,都了解它的作用,還可以從容不迫的繼續閱讀下去。
回到我們的一堆骰子上。我要讓一堆骰子變得更豪華囉!
In [13]: def 一堆骰子(骰子數量):
return {f'骰子{i}': 骰子() for i in range(1, 骰子數量+1)}
In [14]: 一堆骰子(6)
Out[14]: {'骰子1': 5, '骰子2': 1, '骰子3': 5, '骰子4': 6, '骰子5': 1, '骰子6': 1}
這邊我用到了 dictionary comprehension。因為字典物件中的每個項目都必須是「鑰匙(key): 內容(value)」組合,因此在寫 dictionary comprehension 時也要這麼做。
{鑰匙: 內容 for 標籤 in 可數(ˇ)數(ˋ)物件}
除了有 list comprehension、dictionary comprehension 之外,還有 set comprehension。但是沒有 tuple comprehension。因為 tuple 是不可變更清單,我們沒有辦法在創造出 tuple 之後,在一次迴圈一次迴圈的把項目放進 tuple 裡面。
另外這邊我還偷偷用了f{}
這樣的特殊字串,叫f字串(f-string)。跟一般的字串並無不同,只不過它用{}
提供了在字串中放入變數的位置。有興趣的人可以自己玩玩看,而我明天會把字串相關的操作再講解的更仔細。
哇,現在每顆骰子擲出來的點數是不是一目了然呢!
一堆骰子當然不會擲一次就結束,我們可以再多擲幾次:
In [15]: def 一堆骰子一直擲(骰子數量, 擲幾次):
return [{f'骰子{i}': 骰子() for i in range(1, 骰子數量+1)} for j in range(擲幾次)]
In [16]: 一堆骰子一直擲(3, 4)
Out[16]: [{'骰子1': 3, '骰子2': 5, '骰子3': 6},
{'骰子1': 2, '骰子2': 3, '骰子3': 1},
{'骰子1': 4, '骰子2': 4, '骰子3': 6},
{'骰子1': 4, '骰子2': 3, '骰子3': 4}]
上面那個函數,需要輸入兩個參數才能運作。而我們在呼叫的時候,直接一堆骰子一直擲(3, 4)
,不知道大家會不會有疑惑,到底是 3 個骰子還是擲了 3 次呢?Python 很聰明,知道你在輸入參數的時候,如果沒有特別說明,那麼你輸入參數的順序,就跟創造函數時,輸入參數的順序是一樣的。來看看我們怎麼創造一堆骰子一直擲
的:
def 一堆骰子一直擲(骰子數量, 擲幾次):
第一個參數是骰子數量
,第二個參數是擲幾次
。所以3
是骰子數量
的3
,4
是擲幾次
的4
。因此在使用函數,尤其是使用別人函數的時候,使用說明可要看清楚了,才不會搞錯參數順序。Python 把沒有特別說明的參數叫做位置參數(positional argument),意思是,用位置來決定是什麼參數。
注意到,上面有一個但書,叫做如果沒有特別說明。所以如果我特別說明了呢?
In [17]: 一堆骰子一直擲(擲幾次=4, 骰子數量=3)
Out[17]: [{'骰子1': 5, '骰子2': 2, '骰子3': 5},
{'骰子1': 6, '骰子2': 5, '骰子3': 6},
{'骰子1': 6, '骰子2': 6, '骰子3': 1},
{'骰子1': 3, '骰子2': 1, '骰子3': 2}]
沒錯,參數的順序你要怎麼擺就怎麼擺。不過參數名稱你要寫對喔。怎麼看參數名稱呢?按 Shift + Tab,如圖二。
圖二、一堆骰子一直擲
的使用說明書。
而這種寫出參數名稱的,Python 把它叫做關鍵字參數(keyword argument)。你可以把位置參數跟關鍵字參數混著用,但是必須要注意,先寫完位置參數之後,才能再寫關鍵字參數:
In [18]: 一堆骰子一直擲(3, 擲幾次=4)
Out[18]: [{'骰子1': 3, '骰子2': 3, '骰子3': 3},
{'骰子1': 3, '骰子2': 2, '骰子3': 1},
{'骰子1': 2, '骰子2': 3, '骰子3': 6},
{'骰子1': 3, '骰子2': 4, '骰子3': 3}]
In [19]: 一堆骰子一直擲(骰子數量=3, 4)
File "<ipython-input-24-a709ba2623c1>", line 1
一堆骰子一直擲(骰子數量=3, 4)
^
SyntaxError: positional argument follows keyword argument
就說了先輸入位置參數。
在賭場裡,比較主流的擲骰子遊戲叫做CRAPS➂。
crap /kræp/ (noun) 屎,廢物,垃圾。
~節錄自《民明書房大字典》
真不懂為什麼叫這個名字,好吧先不管。CRAPS是用兩個骰子來遊戲的,所以我們準備一堆骰子也沒用,但難道就要把一堆骰子給丟了嗎?
In [20]: def CRAPS(擲幾次, 骰子數量=2):
return [{f'骰子{i}': 骰子() for i in range(1, 骰子數量+1)} for j in range(擲幾次)]
這次我們重新做了一個函數叫CRAPS
,內容都跟一堆骰子一直擲
相同。只有在參數的地方,我先提醒CRAPS
說,骰子數量=2
。這有什麼差別呢?這讓我們之後呼叫CRAPS
,只需要輸入一個參數,也就是擲幾次
就搞定了。但你突然想骰 3 顆骰子,CRAPS
也願意幫你做到。
你或許會想問個小問題?在創造CRAPS
的時候,我怎麼突然把骰子數量
跟擲幾次
這兩個參數的位置調換了呢?因為一但我寫出骰子數量=2
,骰子數量就成了關鍵字參數。關鍵字參數(keyword argument)要放在位置參數(positional argument)後面。
In [21]: CRAPS(5)
Out[21]: [{'骰子1': 5, '骰子2': 3},
{'骰子1': 4, '骰子2': 1},
{'骰子1': 2, '骰子2': 6},
{'骰子1': 2, '骰子2': 2},
{'骰子1': 2, '骰子2': 6}]
CRAPS、CRAPS,我想擲十顆骰子,吶,可以嗎 CRAPS?
In [22]: CRAPS(1, 10)
Out[22]: [{'骰子1': 1,
'骰子2': 4,
'骰子3': 5,
'骰子4': 3,
'骰子5': 1,
'骰子6': 3,
'骰子7': 3,
'骰子8': 3,
'骰子9': 5,
'骰子10': 3}]
CRAPS: As you wish!
好啦,函數我想講的就差不多到這了,但 comprehension 的部分,還有一些有趣的東西可以談談。賭場中 CRAPS 的規則大約是這樣的:在一個回合內,若擲出的兩顆骰子點數相加等於 7 ,就是玩家獲勝。而擲出的兩顆骰子點數相加等於 2 的話,就是 crap,賭場贏了。能不能讓我們的CRAPS
把贏錢的回合醒目的標註起來呢?來試試看吧。
In [23]: def CRAPS(擲幾次, 骰子數量=2):
CRAPS_list = [{f'骰子{i}': 骰子() for i in range(1, 骰子數量+1)} for j in range(擲幾次)]
return ['Lucky Seven!!' if sum(i.values())==7 else i for i in CRAPS_list]
In [24]: CRAPS(10)
Out[24]: ['Lucky Seven!!',
{'骰子1': 5, '骰子2': 5},
{'骰子1': 6, '骰子2': 2},
{'骰子1': 4, '骰子2': 2},
{'骰子1': 3, '骰子2': 3},
{'骰子1': 3, '骰子2': 1},
{'骰子1': 6, '骰子2': 6},
{'骰子1': 4, '骰子2': 1},
{'骰子1': 1, '骰子2': 3},
'Lucky Seven!!']
我們稍微看一下新的CRAPS
函數回傳清單的方法:
return ['Lucky Seven!!' if sum(i.values())==7 else i for i in CRAPS_list]
[物件一 if 判斷句 else 物件二 for 標籤 in 可數(ˇ)數(ˋ)物件]
直接翻譯:「如果判斷句
的條件成立,請在清單中放入物件一
,否則放入物件二
」。因此造就我們新回傳回來的清單裡,碰到兩顆骰子點數將加等於 7 的情況,就大喊一聲Lucky Seven!!
表示慶祝。
那擲到 crap 的話,是不是也該大喊一聲CRAPS!!
?
In [25]: def CRAPS(擲幾次, 骰子數量=2):
CRAPS_list = [{f'骰子{i}': 骰子() for i in range(1, 骰子數量+1)} for j in range(擲幾次)]
return ['Lucky Seven!!' if sum(i.values())==7
else 'CRAPS!!' if sum(i.values())==2
else i for i in CRAPS_list]
In [26]: [{'骰子1': 5, '骰子2': 3},
{'骰子1': 6, '骰子2': 6},
'CRAPS!!',
{'骰子1': 6, '骰子2': 2},
'Lucky Seven!!',
{'骰子1': 4, '骰子2': 1},
{'骰子1': 2, '骰子2': 1},
'Lucky Seven!!',
'Lucky Seven!!',
'Lucky Seven!!',
{'骰子1': 6, '骰子2': 6},
{'骰子1': 2, '骰子2': 6},
{'骰子1': 5, '骰子2': 6},
{'骰子1': 5, '骰子2': 4},
{'骰子1': 1, '骰子2': 3},
{'骰子1': 6, '骰子2': 2},
{'骰子1': 3, '骰子2': 2},
'Lucky Seven!!',
{'骰子1': 2, '骰子2': 1},
{'骰子1': 3, '骰子2': 2}]
CRAPS!! 好了,今天的程式碼我也放在 Github 上了(Github 或 nbviewer),有興趣的可以找來玩玩囉!
➀ 雨人 wiki
➁ 函數 wiki
➂ CRAPS wiki
註:對於此系列文有興趣的讀者,歡迎參考由此系列文擴編成書的 LINE Bot by Python,以及最新的系列文《賴田捕手:追加篇》
第 31 天 初始化 LINE BOT on Heroku
第 32 天 快速回覆 QuickReply 介紹
第 33 天 妥善運用 Heroku APP 暫存空間
第 34 天 妥善運用 LINE Notify 免費推播
第 35 天 製造 Deploy to Heroku 按鈕